﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using Ast.Framework.Caching;
using Ast.Framework.Environment.Extensions.Models;
using Ast.Framework.FileSystems.Dependencies;
using Ast.Framework.FileSystems.Paths;
using Castle.Core.Logging;

namespace Ast.Framework.Environment.Extensions.Loaders
{
    /// <summary>
    /// Load an extension by looking into the "bin" subdirectory of an
    /// extension directory.
    /// </summary>
    public class PrecompiledExtensionLoader : ExtensionLoaderBase
    {
        private readonly IAssemblyProbingFolder _assemblyProbingFolder;
        private readonly IPathProvider _pathProvider;
        private readonly IPathMonitor _pathMonitor;

        public PrecompiledExtensionLoader(IAssemblyProbingFolder assemblyProbingFolder, IPathProvider pathProvider, IPathMonitor pathMonitor)
        {
            _assemblyProbingFolder = assemblyProbingFolder;
            _pathProvider = pathProvider;
            _pathMonitor = pathMonitor;
            Logger = NullLogger.Instance;
        }

        public ILogger Logger { get; set; }

        public override int Order
        {
            get { return 30; }
        }

        public override void ExtensionRemoved(ExtensionLoadingContext ctx, DependencyDescriptor dependency)
        {
            if (_assemblyProbingFolder.AssemblyExists(dependency.Name))
            {
                ctx.DeleteActions.Add(
                    () =>
                    {
                        Logger.InfoFormat("ExtensionRemoved: Deleting assembly \"{0}\" from probing directory", dependency.Name);
                        _assemblyProbingFolder.DeleteAssembly(dependency.Name);
                    });
            }
        }

        public override void ExtensionActivated(ExtensionLoadingContext ctx, ExtensionDescriptor extension)
        {
            var sourceFileName = GetAssemblyPath(extension);

            // Copy the assembly if it doesn't exist or if it is older than the source file.
            var copyAssembly =
                !_assemblyProbingFolder.AssemblyExists(extension.Id) ||
                File.GetLastWriteTimeUtc(sourceFileName) > _assemblyProbingFolder.GetAssemblyDateTimeUtc(extension.Id);

            if (copyAssembly)
            {
                ctx.CopyActions.Add(() => _assemblyProbingFolder.StoreAssembly(extension.Id, sourceFileName));
            }
        }

        public override void ExtensionDeactivated(ExtensionLoadingContext ctx, ExtensionDescriptor extension)
        {
            if (_assemblyProbingFolder.AssemblyExists(extension.Id))
            {
                ctx.DeleteActions.Add(
                    () =>
                    {
                        Logger.InfoFormat("ExtensionDeactivated: Deleting assembly \"{0}\" from probing directory", extension.Id);
                        _assemblyProbingFolder.DeleteAssembly(extension.Id);
                    });
            }
        }

        public override void ReferenceActivated(ExtensionLoadingContext context, ExtensionReferenceProbeEntry referenceEntry)
        {
            if (string.IsNullOrEmpty(referenceEntry.Path))
                return;

            string sourceFileName = referenceEntry.Path;

            // Copy the assembly if it doesn't exist or if it is older than the source file.
            bool copyAssembly =
                !_assemblyProbingFolder.AssemblyExists(referenceEntry.Name) ||
                File.GetLastWriteTimeUtc(sourceFileName) > _assemblyProbingFolder.GetAssemblyDateTimeUtc(referenceEntry.Name);

            if (copyAssembly)
            {
                context.CopyActions.Add(() => _assemblyProbingFolder.StoreAssembly(referenceEntry.Name, sourceFileName));
            }
        }

        public override IEnumerable<ExtensionReferenceProbeEntry> ProbeReferences(ExtensionDescriptor descriptor)
        {
            Logger.InfoFormat("Probing references for module '{0}'", descriptor.Id);

            var assemblyPath = GetAssemblyPath(descriptor);
            if (assemblyPath == null)
                return Enumerable.Empty<ExtensionReferenceProbeEntry>();

            var result = _pathProvider
                .ListFiles(_pathProvider.GetDirectoryName(assemblyPath))
                .Where(s => StringComparer.OrdinalIgnoreCase.Equals(Path.GetExtension(s), ".dll"))
                .Where(s => !StringComparer.OrdinalIgnoreCase.Equals(Path.GetFileNameWithoutExtension(s), descriptor.Id))
                .Select(path => new ExtensionReferenceProbeEntry
                {
                    Descriptor = descriptor,
                    Loader = this,
                    Name = Path.GetFileNameWithoutExtension(path),
                    Path = path
                })
                .ToList();

            Logger.InfoFormat("Done probing references for module '{0}'", descriptor.Id);
            return result;
        }

        public override Assembly LoadReference(DependencyReferenceDescriptor reference)
        {
            Logger.InfoFormat("Loading reference '{0}'", reference.Name);

            var result = _assemblyProbingFolder.LoadAssembly(reference.Name);

            Logger.InfoFormat("Done loading reference '{0}'", reference.Name);
            return result;
        }

        public override bool IsCompatibleWithModuleReferences(ExtensionDescriptor extension, IEnumerable<ExtensionProbeEntry> references)
        {
            // A pre-compiled module is _not_ compatible with a dynamically loaded module
            // because a pre-compiled module usually references a pre-compiled assembly binary
            // which will have a different identity (i.e. name) from the dynamic module.
            bool result = references.All(r => r.Loader.GetType() != typeof(DynamicExtensionLoader));
            if (!result)
            {
                Logger.InfoFormat("Extension \"{0}\" will not be loaded as pre-compiled extension because one or more referenced extension is dynamically compiled", extension.Id);
            }
            return result;
        }

        public override ExtensionProbeEntry Probe(ExtensionDescriptor descriptor)
        {
            Logger.InfoFormat("Probing for module '{0}'", descriptor.Id);

            var assemblyPath = GetAssemblyPath(descriptor);
            if (assemblyPath == null)
                return null;

            var result = new ExtensionProbeEntry
            {
                Descriptor = descriptor,
                Loader = this,
                Path = assemblyPath,
                PathDependencies = new[] { assemblyPath },
            };

            Logger.InfoFormat("Done probing for module '{0}'", descriptor.Id);
            return result;
        }

        public override ExtensionEntry Load(ExtensionDescriptor descriptor)
        {
            Logger.InfoFormat("Start loading pre-compiled extension \"{0}\"", descriptor.Name);

            var assembly = _assemblyProbingFolder.LoadAssembly(descriptor.Id);
            if (assembly == null)
                return null;

            Logger.InfoFormat("Done loading pre-compiled extension \"{0}\": assembly name=\"{1}\"", descriptor.Name, assembly.FullName);

            return new ExtensionEntry
            {
                Descriptor = descriptor,
                Assembly = assembly,
                ExportedTypes = assembly.GetExportedTypes()
            };
        }

        public override void Monitor(ExtensionDescriptor descriptor, Action<IVolatileToken> monitor)
        {
            // If the assembly exists, monitor it
            string assemblyPath = GetAssemblyPath(descriptor);
            if (assemblyPath != null)
            {
                Logger.DebugFormat("Monitoring path \"{0}\"", assemblyPath);
                monitor(_pathMonitor.WhenPathChanges(assemblyPath));
                return;
            }

            // If the assembly doesn't exist, we monitor the containing "bin" folder, as the assembly 
            // may exist later if it is recompiled in Visual Studio for example, and we need to 
            // detect that as a change of configuration.
            var assemblyDirectory = _pathProvider.Combine(descriptor.Location, descriptor.Id, "bin");
            if (_pathProvider.DirectoryExists(assemblyDirectory))
            {
                Logger.DebugFormat("Monitoring path \"{0}\"", assemblyDirectory);
                monitor(_pathMonitor.WhenPathChanges(assemblyDirectory));
            }
        }

        public string GetAssemblyPath(ExtensionDescriptor descriptor)
        {
            var assemblyPath = _pathProvider.Combine(descriptor.Location, descriptor.Id, "bin", descriptor.Id + ".dll");
            if (_pathProvider.FileExists(assemblyPath)) return assemblyPath;
            assemblyPath = _pathProvider.Combine(descriptor.Location, descriptor.Id, "bin\\Debug", descriptor.Id + ".dll");
            if (_pathProvider.FileExists(assemblyPath)) return assemblyPath;
            assemblyPath = _pathProvider.Combine(descriptor.Location, descriptor.Id, "bin\\Release", descriptor.Id + ".dll");
            if (_pathProvider.FileExists(assemblyPath)) return assemblyPath;

            return null;
        }
    }
}